package com.tuuzed.android.eventbus;

import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class EventBus {
    /**
     * 默认使用该模式，表示该方法会在当前发布事件的线程执行
     */
    public static final int POSTING = 0;
    /**
     * 表示会在UI线程中执行
     */
    public static final int MAIN = 1;
    /**
     * 若当前线程非UI线程则在当前线程中执行，否则加入后台任务队列，使用线程池调用
     */
    public static final int BACKGROUND = 2;
    /**
     * 加入后台任务队列，使用线程池调用
     */
    public static final int ASYNC = 3;

    public static class Builder {

        private ExecutorService asyncThreadPool;

        public Builder setBackgroundThreadPool(ExecutorService asyncThreadPool) {
            this.asyncThreadPool = asyncThreadPool;
            return this;
        }

        public EventBus build() {
            if (asyncThreadPool == null) {
                int mThreads = Runtime.getRuntime().availableProcessors() + 1;
                asyncThreadPool = Executors.newFixedThreadPool(mThreads);
            }
            return new EventBus(this);
        }
    }

    private static class DefaultInstanceHolder {
        private static final EventBus defaultInstance = EventBus.builder().build();
    }

    public static Builder builder() {
        return new Builder();
    }

    public static EventBus getDefault() {
        return DefaultInstanceHolder.defaultInstance;
    }

    private Map<Object, List<SubscriberMethod>> mSubscribers;
    private Map<Class<?>, Object> mStickyEvents;

    private Handler mMainThreadHandler;
    private ExecutorService mAsyncThreadPool;

    private EventBus(@NonNull Builder builder) {
        this.mSubscribers = new HashMap<>();
        this.mStickyEvents = new ConcurrentHashMap<>();

        this.mMainThreadHandler = new Handler(Looper.getMainLooper());
        this.mAsyncThreadPool = builder.asyncThreadPool;
    }

    public void register(@NonNull final Object subscriber) {
        List<SubscriberMethod> subscribeMethods = findSubscribeMethods(subscriber);
        if (subscribeMethods.isEmpty()) {
            throw new RuntimeException("No Method @Subscribe");
        }
        Collections.sort(subscribeMethods, (o1, o2) -> {
            if (o1.priority > o2.priority) return -1;
            else if (o1.priority < o2.priority) return 1;
            else return 0;
        });
        synchronized (this) {
            mSubscribers.put(subscriber, subscribeMethods);
        }
        requestStickyEvent(subscriber, subscribeMethods);
    }

    public synchronized void unregister(@NonNull final Object subscriber) {
        mSubscribers.remove(subscriber);
    }

    public void post(@NonNull Object event) {
        post(event, false);
    }

    public void postSticky(@NonNull Object event) {
        post(event, true);
    }

    public boolean hasStickyEvent(@NonNull Class<?> eventType) {
        return mStickyEvents.containsKey(eventType);
    }

    public void requestStickyEvent(@NonNull final Object subscriber) {
        List<SubscriberMethod> subscriberMethods;
        synchronized (this) {
            subscriberMethods = mSubscribers.get(subscriber);
        }
        if (subscriberMethods == null) {
            return;
        }
        requestStickyEvent(subscriber, subscriberMethods);
    }

    public void removeStickyEvent(@NonNull Object event) {
        removeStickyEvent(event.getClass());
    }

    public void removeStickyEvent(@NonNull Class<?> eventType) {
        mStickyEvents.remove(eventType);
    }

    public void removeAllStickyEvent() {
        mStickyEvents.clear();
    }

    private void post(@NonNull final Object event, boolean isSticky) {
        Class<?> eventType = event.getClass();
        if (isSticky) {
            mStickyEvents.put(eventType, event);
        }
        for (Map.Entry<Object, List<SubscriberMethod>> tmpLocal : mSubscribers.entrySet()) {
            final Object subscriber = tmpLocal.getKey();
            List<SubscriberMethod> subscriberMethods = tmpLocal.getValue();
            for (final SubscriberMethod subscriberMethod : subscriberMethods) {
                post(subscriber, subscriberMethod, event, eventType);
            }
        }
    }

    private void post(@NonNull final Object subscriber,
                      @NonNull final SubscriberMethod subscriberMethod,
                      @NonNull final Object event,
                      @NonNull final Class<?> eventType) {
        if (subscriberMethod.eventType != eventType) {
            return;
        }
        switch (subscriberMethod.threadMode) {
            case POSTING:
                subscriberMethod.invoke(subscriber, event);
                break;
            case MAIN:
                runOnMainThread(() -> subscriberMethod.invoke(subscriber, event));
                break;
            case BACKGROUND:
                runOnBackgroundThread(() -> subscriberMethod.invoke(subscriber, event));
                break;
            case ASYNC:
                runOnAsyncThread(() -> subscriberMethod.invoke(subscriber, event));
                break;
            default:
                break;
        }
    }

    private void requestStickyEvent(@NonNull final Object subscriber,
                                    @NonNull final List<SubscriberMethod> subscriberMethods) {

        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            if (subscriberMethod.isSticky) {
                Object event = mStickyEvents.get(subscriberMethod.eventType);
                if (event != null) {
                    Class<?> eventType = event.getClass();
                    post(subscriber, subscriberMethod, event, eventType);
                }
            }
        }
    }

    private List<SubscriberMethod> findSubscribeMethods(Object subscriber) {
        if (subscriber == null) {
            return Collections.emptyList();
        }
        Class<?> subscriberClass = subscriber.getClass();
        Method[] methods = subscriberClass.getMethods();
        List<SubscriberMethod> subscriberMethods = new ArrayList<>();
        for (Method method : methods) {
            SubscriberMethod subscriberMethod = SubscriberMethod.create(method);
            if (subscriberMethod != null) {
                subscriberMethods.add(subscriberMethod);
            }
        }
        return subscriberMethods;
    }

    private void runOnMainThread(@NonNull Runnable runnable) {
        if (isMainThread()) {
            runnable.run();
        } else {
            mMainThreadHandler.post(runnable);
        }
    }

    private void runOnBackgroundThread(@NonNull Runnable runnable) {
        if (!isMainThread()) {
            runnable.run();
        } else {
            runOnAsyncThread(runnable);
        }
    }

    private void runOnAsyncThread(@NonNull Runnable runnable) {
        mAsyncThreadPool.execute(runnable);
    }

    private boolean isMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }
}